import { on } from '/@/utils/domUtils';
import { isServer } from '/@/utils/is';
import type { ComponentPublicInstance, DirectiveBinding, ObjectDirective } from 'vue';

type DocumentHandler = <T extends MouseEvent>(mouseup: T, mousedown: T) => void;

type FlushList = Map<
   HTMLElement,
   {
      documentHandler: DocumentHandler;
      bindingFn: (...args: unknown[]) => unknown;
   }
>;

const nodeList: FlushList = new Map();

let startClick: MouseEvent;

if (!isServer) {
   on(document, 'mousedown', (e: MouseEvent) => (startClick = e));
   on(document, 'mouseup', (e: MouseEvent) => {
      for (const { documentHandler } of nodeList.values()) {
         documentHandler(e, startClick);
      }
   });
}

function createDocumentHandler(el: HTMLElement, binding: DirectiveBinding): DocumentHandler {
   let excludes: HTMLElement[] = [];
   if (Array.isArray(binding.arg)) {
      excludes = binding.arg;
   } else {
      // due to current implementation on binding type is wrong the type casting is necessary here
      excludes.push(binding.arg as unknown as HTMLElement);
   }
   return function (mouseup, mousedown) {
      const popperRef = (
         binding.instance as ComponentPublicInstance<{
            popperRef: Nullable<HTMLElement>;
         }>
      ).popperRef;
      const mouseUpTarget = mouseup.target as Node;
      const mouseDownTarget = mousedown.target as Node;
      const isBound = !binding || !binding.instance;
      const isTargetExists = !mouseUpTarget || !mouseDownTarget;
      const isContainedByEl = el.contains(mouseUpTarget) || el.contains(mouseDownTarget);
      const isSelf = el === mouseUpTarget;

      const isTargetExcluded =
         (excludes.length && excludes.some((item) => item?.contains(mouseUpTarget))) ||
         (excludes.length && excludes.includes(mouseDownTarget as HTMLElement));
      const isContainedByPopper =
         popperRef && (popperRef.contains(mouseUpTarget) || popperRef.contains(mouseDownTarget));
      if (
         isBound ||
         isTargetExists ||
         isContainedByEl ||
         isSelf ||
         isTargetExcluded ||
         isContainedByPopper
      ) {
         return;
      }
      binding.value();
   };
}

const ClickOutside: ObjectDirective = {
   beforeMount(el, binding) {
      nodeList.set(el, {
         documentHandler: createDocumentHandler(el, binding),
         bindingFn: binding.value,
      });
   },
   updated(el, binding) {
      nodeList.set(el, {
         documentHandler: createDocumentHandler(el, binding),
         bindingFn: binding.value,
      });
   },
   unmounted(el) {
      nodeList.delete(el);
   },
};

export default ClickOutside;
